package com.tnovoselec.android.common.io.image;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Callable;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Log;

import com.tnovoselec.android.common.async.Async;
import com.tnovoselec.android.common.async.listener.Outcome;
import com.tnovoselec.android.common.async.notif.NotifierFactory;
import com.tnovoselec.android.common.io.LRUCache;
import com.tnovoselec.android.common.util.Md5Util;


public class ImageCache {

	protected static final String TAG = ImageCache.class.getSimpleName();

	/**
	 * The ImageCallback interface defines a single method used to pass an image
	 * back to the calling object when it has been loaded.
	 */
	public static interface ImageCallback {

		/**
		 * The onImageLoaded method is called by the ImageCache when an image
		 * has been loaded.
		 * 
		 * @param image The requested image in the form of a Drawable object.
		 * @param url The originally requested URL
		 */
		void onImageLoaded(Drawable image, String url);
	}

	private static ImageCache INSTANCE = null;

	/**
	 * Gets the singleton instance of the ImageCache.
	 * 
	 * @return The ImageCache.
	 */
	public synchronized static ImageCache getInstance() {
		if (ImageCache.INSTANCE == null) {
			ImageCache.INSTANCE = new ImageCache();
		}

		return ImageCache.INSTANCE;
	}

	protected final Object lock = new Object();

	protected final LRUCache<String, WeakReference<Drawable>> instanceCache;
	protected final Map<String, List<ImageCallback>> instanceCallbacks;

	public ImageCache() {
		instanceCache = new LRUCache<String, WeakReference<Drawable>>(100); // TODO Maybe should we use WeakHashMap!?
		instanceCallbacks = new HashMap<String, List<ImageCallback>>();
	}

	protected String getHash(String url) {
		return Md5Util.md5(url);
	}

	protected Drawable drawableFromCache(@SuppressWarnings("unused") String url, String hash) {
		Drawable d = null;
		synchronized (lock) {
			if (instanceCache.containsKey(hash)) {
				WeakReference<Drawable> ref = instanceCache.get(hash);
				if (ref != null) {
					d = ref.get();
					if (d == null) {
						instanceCache.remove(hash);
					}
				}
			}
		}

		return d;
	}

	private static final long CACHE_TIMEOUT = 24 * 60 * 60 * 1000L; // 1 day

	protected boolean isTimeout(File file) {
		return file.exists() && ((file.lastModified() + ImageCache.CACHE_TIMEOUT) < System.currentTimeMillis());
	}

	protected Drawable loadSync(String url, String hash, Context context) {
		Drawable drawable = null;
		try {
			File file = new File(context.getCacheDir(), hash);
			if (isTimeout(file)) {
				// TODO Check this synchronization (file deleting)! Will not exist at ImageCachePermanent!
				synchronized (lock) {
					instanceCache.remove(hash);
					file.delete();
				}
			} else {
				drawable = drawableFromCache(url, hash);
			}

			if (drawable == null) {
				if (!file.exists()) {
					InputStream is = null;
					try {
						is = new URL(url + "?rand=" + new Random().nextInt()).openConnection().getInputStream();
						if (file.createNewFile()) { // Will be true!
							FileOutputStream fos = null;
							try {
								fos = new FileOutputStream(file);
								byte[] buffer = new byte[512];
								int size;
								while ((size = is.read(buffer)) > 0) {
									fos.write(buffer, 0, size);
								}
								fos.flush();
							} catch (Exception ex) {
								file.delete();
								throw ex;
							} finally {
								if (fos != null) {
									fos.close();
								}
							}
						}
					} finally {
						if (is != null) {
							try {
								is.close();
							} catch (Exception ex) {
								// DO nothing
							}
						}
					}
				}
				drawable = Drawable.createFromPath(file.getAbsolutePath());

				if (drawable != null) {
					synchronized (lock) {
						instanceCache.put(hash, new WeakReference<Drawable>(drawable));
					}
				}
			}
		} catch (Exception ex) {
			Log.e(ImageCache.TAG, ex.getMessage(), ex);
		}

		return drawable;
	}

	/**
	 * Loads an image from the passed URL and calls the callback method when
	 * the image is done loading.
	 * 
	 * @param url The URL of the target image.
	 * @param callback A ImageCallback object to pass the loaded image. Should not be null.
	 * @param context The context of the new Drawable image.
	 */
	public void loadAsync(final String url, final ImageCallback callback, final Context context) {
		// No need for validation!
		final String hash = getHash(url);

		synchronized (lock) {
			List<ImageCallback> callbacks = instanceCallbacks.get(hash);
			if (callbacks != null) {
				callbacks.add(callback);
				return;
			}

			callbacks = new ArrayList<ImageCallback>();
			callbacks.add(callback);

			instanceCallbacks.put(hash, callbacks);
		}

		Async.doAsync(
				null,
				new Callable<Drawable>() {

					@Override
					public Drawable call() throws Exception {
						return ImageCache.this.loadSync(url, hash, context);
					}
				},
				new Outcome<Void, Drawable>() {

					@Override
					protected void succeeded(Void key, Drawable d) {
						List<ImageCallback> callbacks = null;

						synchronized (lock) {
							callbacks = instanceCallbacks.remove(hash);
						}

						for (ImageCallback imageCallback : callbacks) {
							imageCallback.onImageLoaded(d, url);
						}
					}

					@Override
					protected void failed(Void key, Throwable error) {
						Log.e(ImageCache.TAG, error.getMessage(), error); // Cannot happen!
					}
				},
				NotifierFactory.getBasicNotifier()
				);
	}
}